 /*******************************************************************************
  * Copyright (c) 2005, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  * Chris Aniszczyk (IBM Corp.) - Fixed NPE
  *******************************************************************************/
 package org.eclipse.update.internal.core;

 import java.io.BufferedWriter ;
 import java.io.File ;
 import java.io.FileNotFoundException ;
 import java.io.FileOutputStream ;
 import java.io.IOException ;
 import java.io.InputStream ;
 import java.io.OutputStream ;
 import java.io.OutputStreamWriter ;
 import java.io.PrintWriter ;
 import java.io.UnsupportedEncodingException ;
 import java.net.MalformedURLException ;
 import java.net.URL ;
 import java.util.ArrayList ;
 import java.util.Arrays ;
 import java.util.Date ;
 import java.util.EmptyStackException ;
 import java.util.HashMap ;
 import java.util.List ;
 import java.util.Locale ;
 import java.util.Map ;
 import java.util.ResourceBundle ;
 import java.util.Stack ;
 import java.util.StringTokenizer ;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.update.core.ContentReference;
 import org.eclipse.update.core.IFeature;
 import org.eclipse.update.core.IFeatureReference;
 import org.eclipse.update.core.IImport;
 import org.eclipse.update.core.IIncludedFeatureReference;
 import org.eclipse.update.core.IPlatformEnvironment;
 import org.eclipse.update.core.IPluginEntry;
 import org.eclipse.update.core.InstallMonitor;
 import org.eclipse.update.core.Site;
 import org.eclipse.update.core.SiteManager;
 import org.eclipse.update.core.Utilities;
 import org.eclipse.update.core.model.InstallAbortedException;
 import org.eclipse.update.internal.core.connection.ConnectionFactory;
 import org.eclipse.update.internal.core.connection.IResponse;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
 import org.osgi.service.packageadmin.PackageAdmin;

 /**
  *
  */
 public class UpdateManagerUtils {

     private static boolean OS_UNIX = org.eclipse.osgi.service.environment.Constants.OS_HPUX
             .equals(Platform.getOS())
             || org.eclipse.osgi.service.environment.Constants.OS_AIX
                     .equals(Platform.getOS())
             || org.eclipse.osgi.service.environment.Constants.OS_LINUX
                     .equals(Platform.getOS())
             || org.eclipse.osgi.service.environment.Constants.OS_SOLARIS
                     .equals(Platform.getOS());
     private static FragmentEntry[] noFragments = new FragmentEntry[0];
     private static Map table;

     static {
         table = new HashMap ();
         table.put("compatible", new Integer (IImport.RULE_COMPATIBLE)); //$NON-NLS-1$
 table.put("perfect", new Integer (IImport.RULE_PERFECT)); //$NON-NLS-1$
 table.put("equivalent", new Integer (IImport.RULE_EQUIVALENT)); //$NON-NLS-1$
 table.put("greaterOrEqual", new Integer (IImport.RULE_GREATER_OR_EQUAL)); //$NON-NLS-1$
 }

     private static Writer writer;
     // manage URL to File
 private static Map urlFileMap;

     private static Map localFileFragmentMap;
     private static Stack bufferPool;
     private static final int BUFFER_SIZE = 4096; // 4kbytes
 private static final int INCREMENT_SIZE = 10240; // 10kbytes
 /**
      * return the urlString if it is a absolute URL
      * otherwise, return the default URL if the urlString is null
      * The defaultURL may point ot a file, create a file URL then
      * if the urlString or the default URL are relatives, prepend the rootURL to it
      */
     public static URL getURL(URL rootURL, String urlString, String defaultURL) throws MalformedURLException {
         URL url = null;

         // if no URL , provide Default
 if (urlString == null || urlString.trim().equals("")) { //$NON-NLS-1$

             // no URL, no default, return right now...
 if (defaultURL == null || defaultURL.trim().equals("")) //$NON-NLS-1$
 return null;
             else
                 urlString = defaultURL;
         }

         // URL can be relative or absolute
 if (urlString.startsWith("/") && urlString.length() > 1) //$NON-NLS-1$
 urlString = urlString.substring(1);
         try {
             url = new URL (urlString);
         } catch (MalformedURLException e) {
             // the url is not an absolute URL
 // try relative
 url = new URL (rootURL, urlString);
         }

         return url;
     }

     /**
      * return a relative String to rootURL
      * if url contains rootURL so
      * new URL(rootURL, resultString) == url
      *
      */
     public static String getURLAsString(URL rootURL, URL url) {
         String result = null;

         if (rootURL == null) {
             return (url == null) ? null : url.toString();
         }

         // if no URL , return null
 if (url != null) {

             result = url.toExternalForm();

             if (rootURL.getHost() != null && !rootURL.getHost().equals(url.getHost()))
                 return result;

             if (rootURL.getProtocol() != null && !rootURL.getProtocol().equals(url.getProtocol()))
                 return result;

             if (rootURL.getPort() != url.getPort())
                 return result;

             String rootURLFileString = rootURL.getFile();
             rootURLFileString = rootURLFileString.replace(File.separatorChar, '/');
             if (!rootURLFileString.endsWith("/")) { //$NON-NLS-1$
 int index = rootURLFileString.lastIndexOf('/');
                 if (index != -1) {
                     rootURLFileString = rootURLFileString.substring(0, index);
                 }
             }
             String urlFileString = url.getFile();

             if (urlFileString.startsWith(rootURLFileString)) {
                 result = urlFileString.substring(rootURLFileString.length());
                 result = result.replace(File.separatorChar, '/');
             } else {
                 // we need to check the following
 // file:/C:/ and file:C:/
 if ("file".equalsIgnoreCase(url.getProtocol())) { //$NON-NLS-1$
 File rootFile = new File (rootURLFileString);
                     File urlFile = new File (urlFileString);

                     File relativePath = urlFile;
                     while (relativePath != null && !rootFile.equals(relativePath.getParentFile())) {
                         relativePath = relativePath.getParentFile();
                     }

                     if (relativePath == null) {
                         UpdateCore.warn("Cannot calculate relative path"); //$NON-NLS-1$
 return url.toString();
                     } else {
                         String relativeRootString = relativePath.getParentFile().getAbsolutePath();
                         String fullString = urlFile.getAbsolutePath();
                         if (!fullString.startsWith(relativeRootString)) {
                             UpdateCore.warn("Full path:" + fullString + " does not start with " + relativeRootString); //$NON-NLS-1$ //$NON-NLS-2$
 return url.toString();
                         } else {
                             String returnString = fullString.substring(relativeRootString.length() + 1);
                             if (urlFile.isDirectory())
                                 returnString += File.separator;
                             // we lost the last slash when tranforming in File
 returnString = returnString.replace(File.separatorChar, '/');
                             return returnString;
                         }

                     }

                 } else {
                     result = url.toString();
                 }
             }
         }

         return result;
     }

     /**
      * returns a translated String
      */
     public static String getResourceString(String infoURL, ResourceBundle bundle) {
         String result = null;
         if (infoURL != null) {
             result = Platform.getResourceString(UpdateCore.getPlugin().getBundle(), infoURL, bundle);
         }
         return result;
     }

     /**
      *
      */
     public static URL copyToLocal(InputStream sourceContentReferenceStream, String localName, InstallMonitor monitor) throws MalformedURLException , IOException , InstallAbortedException {
         URL result = null;
         // create the Dir if they do not exist
 // get the path from the File to resolve File.separator..
 // do not use the String as it may contain URL like separator
 File localFile = new File (localName);
         int index = localFile.getPath().lastIndexOf(File.separator);
         if (index != -1) {
             File dir = new File (localFile.getPath().substring(0, index));
             if (!dir.exists())
                 dir.mkdirs();
         }

         // transfer the content of the File
 if (!localFile.isDirectory()) {
             OutputStream localContentReferenceStream = new FileOutputStream (localFile);
             try {
                 Utilities.copy(sourceContentReferenceStream, localContentReferenceStream, monitor);
             } finally {
                 try {
                     localContentReferenceStream.close();
                 } catch (IOException e){}
             }
         }
         result = localFile.toURL();
         return result;
     }

     /*
      * [20305] need to slam permissions for executable libs on some
      * platforms. This is a temporary fix
      */
     public static void checkPermissions(ContentReference ref, String filePath) {

         if (ref.getPermission() != 0) {
             UpdateCore.warn("Change permission for " + filePath + " to " + ref.getPermission()); //$NON-NLS-1$ //$NON-NLS-2$
 // FIXME: change the code to use JNI
 }

         if (filePath != null && OS_UNIX && ref.getPermission() != 0) {
             // add execute permission on shared libraries 20305
 // do not remove write permission 20896
 // chmod a+x *.sl
 try {
                 Process pr = Runtime.getRuntime().exec(new String [] { "chmod", "a+x", filePath }); //$NON-NLS-1$ //$NON-NLS-2$
 Thread chmodOutput = new StreamConsumer(pr.getInputStream());
                 chmodOutput.setName("chmod output reader"); //$NON-NLS-1$
 chmodOutput.start();
                 Thread chmodError = new StreamConsumer(pr.getErrorStream());
                 chmodError.setName("chmod error reader"); //$NON-NLS-1$
 chmodError.start();
             } catch (IOException ioe) {
             }

         }
     }

     /**
      * Returns a random file name for the local system
      * attempt to conserve the extension if there is a '.' in the path
      * and no File.Seperator after the '.'
      *
      * \a\b\c.txt -> c987659385.txt
      * c.txt -> c3854763.txt
      * c -> c953867549
      */
     public static String getLocalRandomIdentifier(String remotePath, Date date) {
         int dotIndex = remotePath.lastIndexOf("."); //$NON-NLS-1$
 int fileIndex = remotePath.lastIndexOf(File.separator);
         // if there is a separator after the dot
 // do not consider it as an extension
 String ext = (dotIndex != -1 && fileIndex < dotIndex) ? remotePath.substring(dotIndex) : ""; //$NON-NLS-1$
 // the name is the string between the separator and the dot
 // if there is no separator, it is the string up to the dot
 // if there is no dot, go to the end of the string
 if (fileIndex == -1)
             fileIndex = 0;
         if (dotIndex == -1)
             dotIndex = remotePath.length();
         // if I have a separator and no dot: /a/b/c -> c
 // if my separator is the last /a/b/c/, fileIndex and dotIndex are the same, so it will return the default temp name
 String name = (fileIndex < dotIndex) ? remotePath.substring(fileIndex, dotIndex) : "Eclipse_Update_TMP_"; //$NON-NLS-1$
 String result = name + date.getTime() + ext;
         return result;
     }

     /**
      * remove a file or directory from the file system.
      * used to clean up install
      */
     public static void removeFromFileSystem(File file) {
         if (!file.exists() || !file.canWrite())
             return;

         if (file.isDirectory()) {
             String [] files = file.list();
             if (files != null) // be careful since file.list() can return null
 for (int i = 0; i < files.length; ++i)
                     removeFromFileSystem(new File (file, files[i]));
         }

         if (!file.delete()) {
             String msg = NLS.bind(Messages.UpdateManagerUtils_UnableToRemoveFile, (new String [] { file.getAbsolutePath() }));
             UpdateCore.log(msg, new Exception ());
         }
     }

     /**
      * remove all the empty directories recursively
      * used to clean up install
      */
     public static void removeEmptyDirectoriesFromFileSystem(File file) {
         if (!file.isDirectory())
             return;

         String [] files = file.list();
         if (files != null) { // be careful since file.list() can return null
 for (int i = 0; i < files.length; ++i) {
                 removeEmptyDirectoriesFromFileSystem(new File (file, files[i]));
             }
         }
         if (!file.delete()) {
             String msg = NLS.bind(Messages.UpdateManagerUtils_UnableToRemoveFile, (new String [] { file.getAbsolutePath() }));
             UpdateCore.log(msg, new Exception ());
         }
     }

     /**
      * Returns the plugin entries that are in source array and
      * not in target array
      */
     public static IPluginEntry[] diff(IPluginEntry[] sourceArray, IPluginEntry[] targetArray) { // No pluginEntry to Install, return Nothing to instal
 if (sourceArray == null || sourceArray.length == 0) {
             return new IPluginEntry[0];
         } // No pluginEntry installed, Install them all
 if (targetArray == null || targetArray.length == 0) {
             return sourceArray;
         } // if a IPluginEntry from sourceArray is NOT in
 // targetArray, add it to the list
 List list1 = Arrays.asList(targetArray);
         List result = new ArrayList (0);
         for (int i = 0; i < sourceArray.length; i++) {
             if (!list1.contains(sourceArray[i]))
                 result.add(sourceArray[i]);
         }

         IPluginEntry[] resultEntry = new IPluginEntry[result.size()];
         if (result.size() > 0)
             result.toArray(resultEntry);
         return resultEntry;
     }

     /**
      * Returns the parent URL of the given URL, or <code>null</code> if the
      * given URL is the root.
      * <table>
      * <caption>Example</caption>
      * <tr>
      * <th>Given URL</th>
      * <th>Parent URL</th>
      * <tr>
      * <td>"http://hostname/"</td>
      * <td>null</td>
      * <tr>
      * <td>"http://hostname/folder/file</td>
      * <td>"http://hostname/folder/</td>
      * </table>
      *
      * @param url a URL
      * @return the parent of the given URL
      */
     public static URL getParent(URL url) {
         String file = url.getFile();
         int len = file.length();
         if (len == 0 || len == 1 && file.charAt(0) == '/')
             return null;
         int lastSlashIndex = -1;
         for (int i = len - 2; lastSlashIndex == -1 && i >= 0; --i) {
             if (file.charAt(i) == '/')
                 lastSlashIndex = i;
         }
         if (lastSlashIndex == -1)
             file = ""; //$NON-NLS-1$
 else
             file = file.substring(0, lastSlashIndex + 1);
         try {
             url = new URL (url.getProtocol(), url.getHost(), url.getPort(), file);
         } catch (MalformedURLException e) {
             Assert.isTrue(false, e.getMessage());
         }
         return url;
     }

     /**
      *
      */
     public static URL asDirectoryURL(URL url) throws MalformedURLException {
         //url = URLEncoder.encode(url);
 String path = url.getFile();
         if (!path.endsWith("/")) { //$NON-NLS-1$
 int index = path.lastIndexOf('/');
             if (index != -1)
                 path = path.substring(0, index + 1);
             // ignore any ref in original URL
 url = new URL (url.getProtocol(), url.getHost(), url.getPort(), path);
         }
         return url;
     }

     /*
      * Compares two URL for equality
      * Return false if one of them is null
      */
     public static boolean sameURL(URL url1, URL url2) {

         if (url1 == null || url2 == null)
             return false;
         if (url1 == url2)
             return true;
         if (url1.equals(url2))
             return true;

         // check if URL are file: URL as we may
 // have 2 URL pointing to the same featureReference
 // but with different representation
 // (i.e. file:/C;/ and file:C:/)
 if (!"file".equalsIgnoreCase(url1.getProtocol())) //$NON-NLS-1$
 return false;
         if (!"file".equalsIgnoreCase(url2.getProtocol())) //$NON-NLS-1$
 return false;

         File file1 = getFileFor(url1);//new File(url1.getFile());
 File file2 = getFileFor(url2);

         if (file1 == null)
             return false;

         return (file1.equals(file2));
     }
     
     /*
      * Method getFileFor.
      * @param url1
      * @return File
      */
     private static File getFileFor(URL url1) {
         if (urlFileMap == null) urlFileMap = new HashMap ();
         if (urlFileMap.get(url1)!=null) return (File )urlFileMap.get(url1);
         File newFile = new File (url1.getFile());
         urlFileMap.put(url1,newFile);
         return newFile;
     }

     /*
      * returns the list of FeatureReference that are parent of
      * the Feature or an empty array if no parent found.
      * @param onlyOptional if set to <code>true</code> only return parents that consider the feature optional
      * @param child
      * @param possiblesParent
      */
     public static IFeatureReference[] getParentFeatures(IFeature childFeature, IFeatureReference[] possiblesParent, boolean onlyOptional) throws CoreException {

         if (childFeature == null)
             return new IFeatureReference[0];

         List parentList = new ArrayList ();
         IIncludedFeatureReference[] children = null;
         IFeature compareFeature = null;
         for (int i = 0; i < possiblesParent.length; i++) {
             try {
                 IFeature possibleParentFeature = possiblesParent[i].getFeature(null);
                 if (possibleParentFeature != null) {
                     children = possibleParentFeature.getIncludedFeatureReferences();
                     for (int j = 0; j < children.length; j++) {
                         try {
                             compareFeature = children[j].getFeature(null);
                         } catch (CoreException e) {
                             UpdateCore.warn("", e); //$NON-NLS-1$
 }
                         if (childFeature.equals(compareFeature)) {
                             if (onlyOptional) {
                                 if (UpdateManagerUtils.isOptional(children[j])) {
                                     parentList.add(possiblesParent[i]);
                                 } else {
                                     UpdateCore.warn("Feature :" + children[j] + " not optional. Not included in parents list."); //$NON-NLS-1$ //$NON-NLS-2$
 }
                             } else {
                                 parentList.add(possiblesParent[i]);
                             }
                         }
                     }
                 }
             } catch (CoreException e) {
                 UpdateCore.warn("", e); //$NON-NLS-1$
 }
         }

         IFeatureReference[] parents = new IFeatureReference[0];
         if (parentList.size() > 0) {
             parents = new IFeatureReference[parentList.size()];
             parentList.toArray(parents);
         }
         return parents;
     }

     /*
      * returns the list of Features that are parent of
      * the Feature or an empty array if no parent found
      * @param onlyOptional if set to <code>true</code> only return parents that consider the feature optional
      * @param child
      * @param possiblesParent
      */
     public static IFeatureReference[] getParentFeatures(IFeatureReference child, IFeatureReference[] possiblesParent, boolean onlyOptional) throws CoreException {

         if (child == null)
             return new IFeatureReference[0];

         IFeature childFeature = null;
         try {
             childFeature = child.getFeature(null);
         } catch (CoreException e) {
             UpdateCore.warn(null, e);
         }

         if (childFeature == null)
             return new IFeatureReference[0];

         return getParentFeatures(childFeature, possiblesParent, onlyOptional);
     }

     /*
      * If the return code of the HTTP connection is not 200 (OK)
      * thow an IO exception
      *
      */
     public static void checkConnectionResult(IResponse response,URL url) throws IOException {
         // did the server return an error code ?
 int result = response.getStatusCode();

         if (result != IStatusCodes.HTTP_OK) {
             String serverMsg = response.getStatusMessage();
             response.close();
             throw new FatalIOException(NLS.bind(Messages.ContentReference_HttpNok, (new Object [] { new Integer (result), serverMsg, url })));
         }
     }

     public static class StreamConsumer extends Thread {
         InputStream is;
         byte[] buf;
         public StreamConsumer(InputStream inputStream) {
             super();
             this.setDaemon(true);
             this.is = inputStream;
             buf = new byte[512];
         }
         public void run() {
             try {
                 int n = 0;
                 while (n >= 0)
                     n = is.read(buf);
             } catch (IOException ioe) {
             }
         }
     }

     /**
      * Return the optional children to install
      * The optional features to install may not all be direct children
      * of the feature.
      * Also include non-optional features
      *
      * @param children all the nested features
      * @param optionalfeatures optional features to install
      * @return IFeatureReference[]
      */
     public static IFeatureReference[] optionalChildrenToInstall(IFeatureReference[] children, IFeatureReference[] optionalfeatures) {

         List optionalChildrenToInstall = new ArrayList ();
         for (int i = 0; i < children.length; i++) {
             IFeatureReference optionalFeature = children[i];
             if (!UpdateManagerUtils.isOptional(optionalFeature)) {
                 optionalChildrenToInstall.add(optionalFeature);
             } else {
                 for (int j = 0; j < optionalfeatures.length; j++) {
                     if (optionalFeature.equals(optionalfeatures[j])) {
                         optionalChildrenToInstall.add(optionalFeature);
                         break;
                     }
                 }
             }
         }

         IFeatureReference[] result = new IFeatureReference[optionalChildrenToInstall.size()];
         if (optionalChildrenToInstall.size() > 0) {
             optionalChildrenToInstall.toArray(result);
         }

         return result;
     }

     /**
      * returns the mapping of matching rules
      * the default returns perfect
      *
      * @since 2.0.2
      */
     public static int getMatchingRule(String rule) {
         if (rule == null)
             return IImport.RULE_COMPATIBLE;
         int ruleInt = ((Integer ) table.get(rule)).intValue();
         if (ruleInt == IImport.RULE_NONE)
             return IImport.RULE_PERFECT;
         return ruleInt;
     }
     
     /**
      * returns the mapping of matching id rules
      * the default returns perfect
      *
      * @since 2.0.2
      */
     public static int getMatchingIdRule(String rule) {
         
         if (rule == null)
             return IImport.RULE_COMPATIBLE;
         if (rule!=null && rule.equalsIgnoreCase("prefix")) //$NON-NLS-1$
 return IImport.RULE_PREFIX;
         return IImport.RULE_PERFECT;
     }
     
     /**
      * Method isOptional.
      * @param featureReference
      * @return boolean
      */
     public static boolean isOptional(IFeatureReference featureReference) {
         if (featureReference==null) return false;
         if (featureReference instanceof IIncludedFeatureReference){
             return ((IIncludedFeatureReference)featureReference).isOptional();
         }
         return false;
     }

     /**
      *
      */
     public static boolean isValidEnvironment(IPlatformEnvironment candidate) {
         if (candidate==null) return false;
         String os = candidate.getOS();
         String ws = candidate.getWS();
         String arch = candidate.getOSArch();
         String nl = candidate.getNL();
         if (os!=null && !isMatching(os, SiteManager.getOS())) return false;
         if (ws!=null && !isMatching(ws, SiteManager.getWS())) return false;
         if (arch!=null && !isMatching(arch, SiteManager.getOSArch())) return false;
         if (nl!=null && !isMatchingLocale(nl, SiteManager.getNL())) return false;
         return true;
     }

     /* Original code - commented out to provide a replacement as per bug 98387
     
     private static boolean isMatching(String candidateValues, String siteValues) {
         if (siteValues==null) return false;
         if ("*".equals(candidateValues)) return true; //$NON-NLS-1$
         if ("".equals(candidateValues)) return true; //$NON-NLS-1$
         siteValues = siteValues.toUpperCase();
         StringTokenizer stok = new StringTokenizer(candidateValues, ","); //$NON-NLS-1$
         while (stok.hasMoreTokens()) {
             String token = stok.nextToken().toUpperCase();
             if (siteValues.indexOf(token)!=-1) return true;
         }
         return false;
     }
     */
     
     /*
      * Fixed bug 98387
      */
     
     private static boolean isMatching(String candidateValues, String siteValues) {
         if (siteValues==null) return false;
         if ("*".equals(candidateValues)) return true; //$NON-NLS-1$
 if ("".equals(candidateValues)) return true; //$NON-NLS-1$
 StringTokenizer siteTokens = new StringTokenizer (siteValues, ","); //$NON-NLS-1$
 //$NON-NLS-1$
 while(siteTokens.hasMoreTokens()) {
             StringTokenizer candidateTokens = new StringTokenizer
                                            (candidateValues, ","); //$NON-NLS-1$
 String siteValue = siteTokens.nextToken();
             while (candidateTokens.hasMoreTokens()) {
                 if (siteValue.equalsIgnoreCase
                                  (candidateTokens.nextToken())) return true;
             }
         }
         return false;
     }

     
     /**
      *
      */
     private static boolean isMatchingLocale(String candidateValues, String locale) {
         if (locale==null) return false;
         if ("*".equals(candidateValues)) return true; //$NON-NLS-1$
 if ("".equals(candidateValues)) return true; //$NON-NLS-1$

         locale = locale.toUpperCase();
         candidateValues = candidateValues.toUpperCase();
         StringTokenizer stok = new StringTokenizer (candidateValues, ","); //$NON-NLS-1$
 while (stok.hasMoreTokens()) {
             String candidate = stok.nextToken();
             if (locale.indexOf(candidate) == 0)
                 return true;
             if (candidate.indexOf(locale) == 0)
                 return true;
         }
         return false;
     }

     
 /**
  * write XML file
  */
 public static class Writer {

     private PrintWriter w;
     private OutputStream out;
     private OutputStreamWriter outWriter;
     private BufferedWriter buffWriter;
     private String encoding;

     /*
      *
      */
     private Writer() {
         super();
     }
     
     public void init(File file, String encoding) throws FileNotFoundException , UnsupportedEncodingException {
         this.encoding = encoding;
         out = new FileOutputStream (file);
         outWriter = new OutputStreamWriter (out, encoding);
         buffWriter = new BufferedWriter (outWriter);
         w = new PrintWriter (buffWriter);
     }

     
     /*
      *
      */
     public void write(IWritable element) {
         w.println("<?xml version=\"1.0\" encoding=\""+encoding+"\"?>"); //$NON-NLS-1$ //$NON-NLS-2$
 w.println(""); //$NON-NLS-1$
 w.println("<!-- File written by Update manager 2.0 -->"); //$NON-NLS-1$
 w.println("<!-- comments in this file are not preserved -->"); //$NON-NLS-1$
 w.println(""); //$NON-NLS-1$
 element.write(0, w);
         close();
     }
     
     /*
      *
      */
      public void close(){
             w.close();
      }

     /*
      *
      */
     private static void appendEscapedChar(StringBuffer buffer, char c) {
         String replacement = getReplacement(c);
         if (replacement != null) {
             buffer.append('&');
             buffer.append(replacement);
             buffer.append(';');
         } else {
             if ((c >= ' ' && c <= 0x7E) || c == '\n' || c == '\r' || c == '\t') {
                 buffer.append(c);
             } else {
                 buffer.append("&#"); //$NON-NLS-1$
 buffer.append(Integer.toString(c));
                 buffer.append(';');
             }
         }
     }

     /*
      *
      */
     public static String xmlSafe(String s) {
         StringBuffer result = new StringBuffer (s.length() + 10);
         for (int i = 0; i < s.length(); ++i)
             appendEscapedChar(result, s.charAt(i));
         return result.toString();
     }
     
     /*
      *
      */
     private static String getReplacement(char c) {
         // Encode special XML characters into the equivalent character references.
 // These five are defined by default for all XML documents.
 switch (c) {
             case '<' :
                 return "lt"; //$NON-NLS-1$
 case '>' :
                 return "gt"; //$NON-NLS-1$
 case '"' :
                 return "quot"; //$NON-NLS-1$
 case '\'' :
                 return "apos"; //$NON-NLS-1$
 case '&' :
                 return "amp"; //$NON-NLS-1$
 }
         return null;
     }
 }
     public static Writer getWriter(File file,String encoding) throws FileNotFoundException , UnsupportedEncodingException {
         if (writer==null) writer = new Writer ();
         writer.init(file,encoding);
         return writer;
     }
     
     public static boolean isSameTimestamp(URL url, long timestamp) {
         try {
             if (UpdateCore.getPlugin().getUpdateSession().isVisited(url)) {
                 return true;
             }
             URL resolvedURL = URLEncoder.encode(url);
             IResponse response = ConnectionFactory.get(resolvedURL);
             long remoteLastModified = response.getLastModified();
             // 2 seconds tolerance, as some OS's may round up the time stamp
 // to the closest second. For safety, we make it 2 seconds.
 return Math.abs(remoteLastModified - timestamp)/1000 <= 2;
         } catch (MalformedURLException e) {
             return false;
         } catch (IOException e) {
             return false;
         }
     }
     /**
      * The file is associated with a lookup key.
      * @param key optional lookup key, or <code>null</code>.
      * @param temp the local working file
      */
     public synchronized static void mapLocalFileFragment(String key, FileFragment temp) {
         // create file association
 if (key != null) {
             if (localFileFragmentMap == null)
                 localFileFragmentMap = new HashMap ();
             localFileFragmentMap.put(key, temp);
         }
     }

     /**
      * The file is associated with a lookup key.
      * @param key optional lookup key, or <code>null</code>.
      */
     public synchronized static void unMapLocalFileFragment(String key) {
         // remove file association
 if (key != null && localFileFragmentMap !=null) {
             localFileFragmentMap.remove(key);
         }
     }
     
     /**
      * Returns a previously cached local file (in temporary area) matching the
      * specified key.
      *
      * @param key lookup key
      * @return cached file, or <code>null</code>.
      */
     public static synchronized FileFragment lookupLocalFileFragment(String key) {
         if (localFileFragmentMap == null)
             return null;
         return (FileFragment) localFileFragmentMap.get(key);
     }
     
     /**
      * Copies specified input stream to the output stream. Neither stream
      * is closed as part of this operation.
      *
      * @param is input stream
      * @param os output stream
      * @param monitor progress monitor
      * @param expectedLength - if > 0, the number of bytes from InputStream will be verified
      * @@return the offset in the input stream where copying stopped. Returns -1 if end of input stream is reached.
      * @since 2.0
      */
     public static long copy(InputStream is, OutputStream os, InstallMonitor monitor, long expectedLength) {
         byte[] buf = getBuffer();
         long offset=0;
         try {
             int len = is.read(buf);
             int nextIncrement = 0;
             while (len != -1) {
                 os.write(buf, 0, len);
                     offset += len;
                 if (monitor != null) {
                     nextIncrement += len;
                     // update monitor periodically
 if (nextIncrement >= INCREMENT_SIZE){
                         monitor.incrementCount(nextIncrement);
                         nextIncrement = 0;
                     }
                     if (monitor.isCanceled()) {
                         return offset;
                     }
                 }
                 if (expectedLength > 0 && offset == expectedLength) {
                     // everything read do not return offset, otherwise trying
 // to read again from this offset will result in HTTP 416
 break;
                 }
                 
                 len = is.read(buf);
             }
             if (nextIncrement > 0 && monitor != null)
                 monitor.incrementCount(nextIncrement);
             if(expectedLength>0 && offset!=expectedLength)
                 throw new IOException (NLS.bind(Messages.UpdateManagerUtils_inputStreamEnded, (new String [] { String.valueOf(offset), String.valueOf(expectedLength) })));
             return -1;
         } catch(IOException e){
             // Log the actual error, as this is no longer
 // passed up the calling stack
 UpdateCore.log(Messages.UpdateManagerUtils_copy + offset, e);
             return offset;
         } finally {
             freeBuffer(buf);
         }
     }

     public static class CopyException extends Exception {
         
         private static final long serialVersionUID = 1L;
         Exception rootException;
         int bytesCopied;

         /**
          *
          */
         public CopyException(Exception rootException, int bytesCopied) {
             super();
             this.rootException= rootException;
             this.bytesCopied=bytesCopied;
         }
         /**
          * Instance of IOException or InstallAbortedException
          * @return
          */
         public Exception getRootException(){
             return rootException;
         }
         public int getBytesCopied(){
             return bytesCopied;
         }

     }
     
     private static synchronized byte[] getBuffer() {
         if (bufferPool == null) {
             return new byte[BUFFER_SIZE];
         }

         try {
             return (byte[]) bufferPool.pop();
         } catch (EmptyStackException e) {
             return new byte[BUFFER_SIZE];
         }
     }

     private static synchronized void freeBuffer(byte[] buf) {
         if (bufferPool == null)
             bufferPool = new Stack ();
         bufferPool.push(buf);
     }
     
     
     /**
     * Returns a list of fragments. Zero length if no fragments.
     * @param bundle the bundle to get fragments for
     */
    public static FragmentEntry[] getFragments(Bundle bundle) {
        PackageAdmin pkgAdmin = UpdateCore.getPlugin().getPackageAdmin();
        Bundle [] fragmentBundles = pkgAdmin.getFragments(bundle);
        if (fragmentBundles == null)
            return noFragments;
        
        FragmentEntry[] fragments = new FragmentEntry[fragmentBundles.length];
        for (int i = 0; i < fragments.length; i++) {
            fragments[i] = new FragmentEntry((String ) fragmentBundles[i]
                    .getHeaders().get(Constants.BUNDLE_SYMBOLICNAME),
                    (String ) fragmentBundles[i].getHeaders().get(
                            Constants.BUNDLE_VERSION), Platform
                            .getResourceString(fragmentBundles[i],
                                    (String ) fragmentBundles[i].getHeaders()
                                            .get(Constants.BUNDLE_VERSION)),
                    fragmentBundles[i].getLocation());
        }
        return fragments;
    }
    
    public static String getWritableXMLString(String value) {
        StringBuffer buf = new StringBuffer ();
        if(value == null)
            return buf.toString();
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            switch (c) {
                case '&' :
                    buf.append("&amp;"); //$NON-NLS-1$
 break;
                case '<' :
                    buf.append("&lt;"); //$NON-NLS-1$
 break;
                case '>' :
                    buf.append("&gt;"); //$NON-NLS-1$
 break;
                case '\'' :
                    buf.append("&apos;"); //$NON-NLS-1$
 break;
                case '\"' :
                    buf.append("&quot;"); //$NON-NLS-1$
 break;
                default :
                    buf.append(c);
                    break;
            }
        }
        return buf.toString();
    }
    
    public static LiteFeature[] getLightFeatures(ExtendedSite site) {
        
        URL fullDigestURL;
        try {
            fullDigestURL = getFullDigestURL( site, Locale.getDefault().getCountry(), Locale.getDefault().getLanguage());
        } catch (MalformedURLException e) {
            UpdateCore.log("Could not access digest on the site: " + e.getMessage(), null); //$NON-NLS-1$
 return null;
        }
        
        Digest digest = new Digest( fullDigestURL);
        try {
            LiteFeature[] features = (LiteFeature[])digest.parseDigest();
            for(int i = 0; i < features.length; i++) {
                features[i].setSite(site);
            }
            return features;
        } catch(Exception e){
            UpdateCore.log("Digest could not be parsed:" + e.getMessage(), null); //$NON-NLS-1$
 return null;
        }
    }
    
    private static URL getFullDigestURL(ExtendedSite site, String country, String language) throws MalformedURLException {
        
        String digestURL = (site.getDigestURL().endsWith("/")? site.getDigestURL(): site.getDigestURL() + "/"); //$NON-NLS-1$ //$NON-NLS-2$

        if (digestURL.indexOf("://") == -1) { //$NON-NLS-1$
 String siteURL = site.getLocationURL().toExternalForm();
            if (siteURL.endsWith(Site.SITE_XML)) {
                siteURL = siteURL.substring(0, siteURL.length() - Site.SITE_XML.length());
            }
            if (digestURL.equals("/")) { //$NON-NLS-1$
 digestURL = siteURL;
            } else {
                if (digestURL.startsWith("/")) { //$NON-NLS-1$
 digestURL = digestURL.substring(1, digestURL.length());
                }
                digestURL = siteURL + digestURL;
            }
        }
        
        digestURL += "digest"; //$NON-NLS-1$

        if ( isLocalSupported(site, country, language)) {
            return new URL (digestURL + "_" + language + "_" + country + ".zip"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 }
        if ( isLangaugeSupported(site, language)) {
            return new URL (digestURL + "_" + language + ".zip"); //$NON-NLS-1$ //$NON-NLS-2$
 }
        return new URL (digestURL + ".zip"); //$NON-NLS-1$
 }

    private static boolean isLangaugeSupported(ExtendedSite site, String language) {
        String [] availableLanguages = site.getAvailableLocals();
        if ((availableLanguages == null) || (availableLanguages.length == 0)) {
            return false;
        }
        for(int i = 0; i < availableLanguages.length; i++) {
            if (availableLanguages[i].equals(language)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isLocalSupported(ExtendedSite site, String country, String language) {
        String localeCode = language + "_" + country; //$NON-NLS-1$
 String [] availableLocals = site.getAvailableLocals();
        if ((availableLocals == null) || (availableLocals.length == 0)) {
            return false;
        }
        for(int i = 0; i < availableLocals.length; i++) {
            if (availableLocals[i].equals(localeCode)) {
                return true;
            }
        }
        return false;
    }
}

